在部分系统模块中 (例如 fs),你可能会奇怪为什么有 fs.readFilefs.readFileSyncfs.promises.readFile 3 种方式。

实际上对应着 3 种编码方式,最初只有同步 (sync) 和异步回调 (callback) 两种方式,在 ES6 (ES2015) 推出之后才有的 async/awaitPromise

  1. 同步操作:同步操作会阻塞当前线程,直到操作完成。因此,在执行耗时操作时,应避免使用同步操作方式,因为它可能导致应用程序的响应性变差

  2. 异步回调函数 (Callbacks):最初的异步执行依托于回调函数实现,回调函数是一个作为参数传递给另一个函数的函数,当异步操作完成时,回调函数会被调用,但当多层嵌套的时候会导致回调地狱;

  3. 异步方法 (Async/Await):ES6 引入的新语法可让我们更方便地写异步代码,并且避免了回调地狱。

下面举一些实际的例子来帮助理解。

1 同步方法

1.1 常见的同步方法

下面是最常见的同步方法,

js function hello(){ console.log('hello world') }

代码会一行行的执行,不会跳着执行。

js console.log(1) hello() console.log(2)

1.2 实现同步 fetch

通常在带有异步方法的情况下,内容输出时机与异步方法执行时间有关,

比如下面的打印 123

```js const { readFile } = require('fs')

console.log(1) readFile(__filename, () => { console.log(3) }) console.log(2) ```

如果要输出 132 咱们只能使用 readFileSync 方法,

那么如何实现一个类似的 Sync 方法呢?我们以 fetch 为例,实现一个 fetchSync,

可以结合使用 child_process.spawnSync 创建子进程来实现一个简单的 fetchSync。

```js const { spawnSync } = require('child_process')

function fetchSync(url, options = {}) { // 将请求参数和 URL 传递给子进程 const child = spawnSync(process.argv[0], [ './_fetch-sync.js', JSON.stringify(url), JSON.stringify(options) ])

// 子进程的标准输出即为请求结果 const result = child.stdout.toString() const responseData = JSON.parse(result) return responseData.body } ```

子进程代码如下 _fetch-sync.js

```js // 如果当前文件是被子进程执行的 if (process.argv.length === 4) { // 从子进程的参数中获取请求参数 const url = JSON.parse(process.argv[2]) const options = JSON.parse(process.argv[3])

fetch(url, options).then(async (response) => { // 将响应数据发送给父进程 const result = { status: response.status, body: await response.json() } console.log(JSON.stringify(result)) }) } ```

运行结果如下。

js console.log(1) console.log( fetchSync( 'https://api.juejin.cn/content_api/v1/content/article_rank?category_id=1&type=hot&count=3&from=1&aid=2608&uuid=7145810834685003271&spider=0' ) ) console.log(2)

2 异步回调函数

2.1 使用示例

读取文件的同时,可以执行其它代码,

```js import fs from 'fs'

console.log(1) fs.readFile('./index.mjs', (err, data) => { console.log(2, data.toString()) }) console.log(3) ```

但如果在回调里还要执行异步的回调就会有回调地狱的问题,下面是一个例子。

2.2 回调地狱

比如要顺序读取 3 个文件,使用回调的方式就需要像下面这样书写,非常的不优雅。

```js import fs from 'fs'

const fileA = 'index.mjs' const fileB = 'index.mjs' const fileC = 'index.mjs' fs.readFile(fileA, (err, dataA) => { console.log('File A content:')

fs.readFile(fileB, (err, dataB) => { console.log('File B content:')

fs.readFile(fileC, (err, dataC) => {
  console.log('File C content:')
})

}) }) ```

在实际复杂的业务场景中会导致难以维护代码,

下面看看 async/await 如何解决这个问题。

3 async/await

利用 async/await 配合 Promise 可以很优雅的处理异步代码。

```js import fs from 'fs/promises'

const fileA = 'index.mjs' const fileB = 'index.mjs' const fileC = 'index.mjs' async function main() { const dataA = await fs.readFile(fileA) console.log('File A content:') const dataB = await fs.readFile(fileB) console.log('File B content:') const dataC = await fs.readFile(fileC) console.log('File C content:') }

main() ```

小结

本节介绍了同步方法、异步回调函数、async/await 三种编码方式的异同:

在介绍同步方法时,介绍了如何利用 child_process.spawnSync 实现一个同步方法 fetchSync

在介绍异步回调函数时,介绍了回调地狱的问题,

最后通过 async/await 优雅的编写异步调用代码。